package compareResult.datasetCompare;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

import javax.imageio.ImageIO;

/**
  * Metric
  *
  * Copyright (C) 2010 Supermap
  *
  *
  * This program is free software; you can redistribute it and/or modify it under the terms of the
  * GNU General Public License as published by the Free Software Foundation; either version 2 of the License,
  * or (at your option) any later version.
  *
  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
  * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  * See the GNU General Public License for more details.
  *
  * You should have received a copy of the GNU General Public License along with this program;
  * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  * 
  */

public class Metric {

	public static boolean verbose = false;   //Turns on verbose mode
	public static double fov = 45.0f;        //Field of view in degrees (0.1 to 89.9)
	public static int threshold = 100;       //#pixels p below which differences are ignored
	public static double luminance = 100.0f; //White luminance (default 100.0 cdm^-2)
	public static double gamma = 2.2f;       //Value to convert rgb into linear space (default 2.2) 
	public static boolean luminanceOnly = false; //Only consider luminance; ignore chroma (color) in the comparison
	public static double colorfactor = 0.0f;     //How much of color to use, 0.0 to 1.0, 0.0 = ignore color
	// int downsample = 0;                       // How many powers of two to down sample the image.
	
	private final static double M_PI = Math.PI;
	private final static int MAX_PYR_LEVELS = 8;
	private static String errorStr = "";

	/**
	 * Given the adaptation luminance, this function returns the threshold of
	 * visibility in cd per m^2 TVI means Threshold vs Intensity function This
	 * version comes from Ward Larson Siggraph 1997
	 */

	private static double tvi(double adaptation_luminance) {
		// returns the threshold luminance given the adaptation luminance
		// units are candelas per meter squared
		double log_a, r, result;
		log_a = Math.log10(adaptation_luminance);

		if (log_a < -3.94d) {
			r = -2.86d;
		} else if (log_a < -1.44d) {
			r = Math.pow(0.405d * log_a + 1.6d, 2.18d) - 2.86d;
		} else if (log_a < -0.0184d) {
			r = log_a - 0.395d;
		} else if (log_a < 1.9d) {
			r = Math.pow(0.249d * log_a + 0.65f, 2.7d) - 0.72d;
		} else {
			r = log_a - 1.255d;
		}

		result = Math.pow(10.0d, r);

		return result;

	}

	/**
	 * computes the contrast sensitivity function (Barten SPIE 1989)
	 * given the cycles per degree (cpd) and luminance (lum)
	 * @param cpd
	 * @param lum
	 * @return
	 */
	private static double csf(double cpd, double lum) {
		double a, b, result;

		a = 440.0d * Math.pow((1.0d + 0.7d / lum), -0.2d);
		b = 0.3d * Math.pow((1.0d + 100.0d / lum), 0.15d);

		result = a * cpd * Math.exp(-b * cpd)
				* Math.sqrt(1.0d + 0.06d * Math.exp(b * cpd));

		return result;
	}

	/**
	 * Visual Masking Function from Daly 1993
	 */
	private static double mask(double contrast) {
		double a, b, result;
		a = Math.pow(392.498d * contrast, 0.7d);
		b = Math.pow(0.0153d * a, 4.0d);
		result = Math.pow(1.0d + b, 0.25d);

		return result;
	}
	
	/**
	  * convert Adobe RGB (1998) with reference white D65 to XYZ
	  */
	private static XYZImage adobeRGBToXYZ(double r, double g, double b) {
		
		// matrix is from http://www.brucelindbloom.com/
		XYZImage xyzImage = new XYZImage();
		xyzImage.setX(r * 0.576700f + g * 0.185556f + b * 0.188212f);
		xyzImage.setY(r * 0.297361f + g * 0.627355f + b * 0.0752847f);
		xyzImage.setZ(r * 0.0270328f + g * 0.0706879f + b * 0.991248f);
		return xyzImage;
	}
	/**
	  * convert XYZ to LAB
	  */
	private static LABImage xYZToLAB(XYZImage xyzImage) {
		double xw = -1;
		double yw = 0;
		double zw = 0;

		// reference white
		XYZImage xyzImage1 = new XYZImage(xw, yw, zw);

		if (xyzImage1.getX() < 0) {
			xyzImage1 = adobeRGBToXYZ(1, 1, 1);
		}
		double epsilon = 216.0f / 24389.0f;
		double kappa = 24389.0f / 27.0f;
		double f[] = new double[3];
		double r[] = new double[3];
		r[0] = xyzImage.getX() / xyzImage1.getX();
		r[1] = xyzImage.getY() / xyzImage1.getY();
		r[2] = xyzImage.getZ() / xyzImage1.getZ();
		for (int i = 0; i < 3; i++) {
			if (r[i] > epsilon) {
				f[i] = (double) Math.pow(r[i], 1.0f / 3.0f);
			} else {
				f[i] = (kappa * r[i] + 16.0f) / 116.0f;
			}
		}
		LABImage labImage = new LABImage();
		labImage.setL(116.0f * f[1] - 16.0f);
		labImage.setA(500.0f * (f[0] - f[1]));
		labImage.setB(200.0f * (f[1] - f[2]));

		return labImage;
	}

	/**
	 * return the Error Info
	 * 
	 * @return
	 */
	public static String getErrorStr() {
		return errorStr;
	}
	
	/**
	 * Compare Picture 只采用精确对比
	 * 
	 * @param bi1
	 * @param bi2
	 * @return
	 * @throws IOException
	 */
	public static boolean comparePic2(String path1, String path2)
			throws IOException {

		File imgFile1 = new File(path1);
		File imgFile2 = new File(path2);

		//当文件指向同一文件时，返回true
		if(imgFile1.getAbsolutePath().equals(imgFile2.getAbsoluteFile())){
			return true;
		}
		FileInputStream fis1 = new FileInputStream(imgFile1);
		byte[] bytes1 = new byte[fis1.available()];
		fis1.read(bytes1);
		fis1.close();

		FileInputStream fis2 = new FileInputStream(imgFile2);
		byte[] bytes2 = new byte[fis2.available()];
		fis2.read(bytes2);
		fis2.close();

		// 精确对比，成功后直接返回true
		boolean identical = true;
		if (bytes1.length == bytes2.length) {
			for (int i = 0; i < bytes1.length; i++) {
				if (bytes1[i] != bytes2[i]) {
					identical = false;
					break;
				}
			}
		} else {
			identical = false;
		}
		if (identical) {
			errorStr = "Images are binary identical\n";
			return true;
		}
		
		//逐一像素对比
		BufferedImage bi1 = ImageIO.read(imgFile1);
		BufferedImage bi2 = ImageIO.read(imgFile2);

		//如果像素长宽不等直接返回false，即图像不一致
		if ((bi1.getWidth() != bi2.getWidth())
				|| (bi1.getHeight() != bi2.getHeight())) {
			errorStr = "Image dimensions do not match\n";
			return false;
		}
        Boolean compareimage = compareImage(bi1,bi2);
        return compareimage;
	}

	//获取图片像素集合
	public static String[][] getPX(BufferedImage bi) {  
        int[] rgb = new int[3];  
     
        int width = bi.getWidth();  
        int height = bi.getHeight();  
        int minx = bi.getMinX();  
        int miny = bi.getMinY();  
        String[][] list = new String[width][height];  
        for (int i = minx; i < width; i++) {  
            for (int j = miny; j < height; j++) {  
                int pixel = bi.getRGB(i, j);  
                rgb[0] = (pixel & 0xff0000) >> 16;  
                rgb[1] = (pixel & 0xff00) >> 8;  
                rgb[2] = (pixel & 0xff);  
                list[i][j] = rgb[0] + "," + rgb[1] + "," + rgb[2];  
            }  
        }  
        rgb = null;  
        return list;  
   
    } 
	
	//逐一像素对比
	public static boolean compareImage(BufferedImage img1, BufferedImage img2) 
	{  
        // 分析图片相似度 begin  
        String[][] list1 = getPX(img1);  
        String[][] list2 = getPX(img2);  
        int xiangsi = 0;  
        int busi = 0;  
        int i = 0, j = 0;  
        for (String[] strings : list1) {  
            if ((i + 1) == list1.length) {  
                continue;  
            }  
            for (int m = 0; m < strings.length; m++) {  
                try {  
                    String[] value1 = list1[i][j].toString().split(",");  
                    String[] value2 = list2[i][j].toString().split(",");  
                    int k = 0;  
                    for (int n = 0; n < value2.length; n++) {  
                        if (Math.abs(Integer.parseInt(value1[k])  
                                - Integer.parseInt(value2[k])) < 5) {  
                            xiangsi++;  
                        } else {  
                            busi++;  
                        }  
                    }  
                } catch (RuntimeException e) {  
                    continue;  
                }  
                j++;  
            }  
            i++;  
        }  
   
        list1 = list2;  
        list2 = list1;  
        i = 0;  
        j = 0;  
        for (String[] strings : list1) {  
            if ((i + 1) == list1.length) {  
                continue;  
            }  
            for (int m = 0; m < strings.length; m++) {  
                try {  
                    String[] value1 = list1[i][j].toString().split(",");  
                    String[] value2 = list2[i][j].toString().split(",");  
                    int k = 0;  
                    for (int n = 0; n < value2.length; n++) {  
                        if (Math.abs(Integer.parseInt(value1[k])  
                                - Integer.parseInt(value2[k])) < 5) {  
                            xiangsi++;  
                        } else {  
                            busi++;  
                        }  
                    }  
                } catch (RuntimeException e) {  
                    continue;  
                }  
                j++;  
            }  
            i++;  
        }  
        String baifen = "";  
        try {  
            baifen = ((Double.parseDouble(xiangsi + "") / Double  
                    .parseDouble((busi + xiangsi) + "")) + "");  
            baifen = baifen.substring(baifen.indexOf(".") + 1, baifen  
                    .indexOf(".") + 3);  
        } catch (Exception e) {  
            baifen = "0";  
        }  
        if (baifen.length() <= 0) {  
            baifen = "0";  
        }  
        if (busi == 0) {  
            baifen = "100";  
        }  
           
        list1 = null;  
        list2 = null;  
           
        if (!baifen.equals("100")) {  
            return false;  
        } else {  
            return true;  
        }  
   
    }

	/**
	 * Compare Picture
	 * 
	 * @param bi1
	 * @param bi2
	 * @return
	 * @throws IOException
	 */
	public static boolean comparePic(String path1, String path2)
			throws IOException {

		File imgFile1 = new File(path1);
		File imgFile2 = new File(path2);

		//当文件指向同一文件时，返回true
		if(imgFile1.getAbsolutePath().equals(imgFile2.getAbsoluteFile())){
			return true;
		}
		FileInputStream fis1 = new FileInputStream(imgFile1);
		byte[] bytes1 = new byte[fis1.available()];
		fis1.read(bytes1);
		fis1.close();

		FileInputStream fis2 = new FileInputStream(imgFile2);
		byte[] bytes2 = new byte[fis2.available()];
		fis2.read(bytes2);
		fis2.close();

		// 精确对比，成功后直接返回true
		boolean identical = true;
		if (bytes1.length == bytes2.length) {
			for (int i = 0; i < bytes1.length; i++) {
				if (bytes1[i] != bytes2[i]) {
					identical = false;
					break;
				}
			}
		} else {
			identical = false;
		}
		if (identical) {
			errorStr = "Images are binary identical\n";
			return true;
		}

		// 精确对比失败，使用模糊对比
		BufferedImage bi1 = ImageIO.read(imgFile1);
		BufferedImage bi2 = ImageIO.read(imgFile2);

		if ((bi1.getWidth() != bi2.getWidth())
				|| (bi1.getHeight() != bi2.getHeight())) {
			errorStr = "Image dimensions do not match\n";
			return false;
		}

		int i, dim;
		dim = bi1.getWidth() * bi1.getHeight();

		// assuming colorspaces are in Adobe RGB (1998) convert to XYZ
		XYZImage[] a_xyzImages = new XYZImage[dim];
		XYZImage[] b_xyzImages = new XYZImage[dim];

		LABImage[] a_labImages = new LABImage[dim];
		LABImage[] b_labImages = new LABImage[dim];
		if (verbose)
			System.out.println("Converting RGB to XYZ\n");

		int x, y, w, h;
		w = bi1.getWidth();
		h = bi1.getHeight();
		for (y = 0; y < h; y++) {
			for (x = 0; x < w; x++) {
				double r, g, b;
				i = x + y * w;
				r = Math.pow(((bi1.getRGB(x, y) & 0xff0000) >> 16) / 255.0f,
						gamma);
				g = Math.pow(((bi1.getRGB(x, y) & 0xff00) >> 8) / 255.0f, gamma);
				b = Math.pow((bi1.getRGB(x, y) & 0xff) / 255.0f, gamma);

				a_xyzImages[i] = adobeRGBToXYZ(r, g, b);
				a_labImages[i] = xYZToLAB(a_xyzImages[i]);

				r = Math.pow(((bi2.getRGB(x, y) & 0xff0000) >> 16) / 255.0f,
						gamma);
				g = Math.pow(((bi2.getRGB(x, y) & 0xff00) >> 8) / 255.0f, gamma);
				b = Math.pow((bi2.getRGB(x, y) & 0xff) / 255.0f, gamma);

				b_xyzImages[i] = adobeRGBToXYZ(r, g, b);
				b_labImages[i] = xYZToLAB(b_xyzImages[i]);
				a_labImages[i].setL(a_xyzImages[i].getY() * luminance);
				b_labImages[i].setL(b_xyzImages[i].getY() * luminance);
			}
		}

		if (verbose)
			System.out.println("Constructing Laplacian Pyramids\n");

		LPyramid la = new LPyramid(a_labImages, w, h);
		LPyramid lb = new LPyramid(b_labImages, w, h);

		double num_one_degree_pixels = (2 * Math.tan(fov * 0.5 * M_PI / 180) * 180 / M_PI);
		double pixels_per_degree = w / num_one_degree_pixels;

		if (verbose)
			System.out.println("Performing test\n");

		double num_pixels = 1;
		int adaptation_level = 0;
		for (i = 0; i < MAX_PYR_LEVELS; i++) {
			adaptation_level = i;
			if (num_pixels > num_one_degree_pixels)
				break;
			num_pixels *= 2;
		}

		double cpd[] = new double[MAX_PYR_LEVELS];
		cpd[0] = 0.5f * pixels_per_degree;
		for (i = 1; i < MAX_PYR_LEVELS; i++)
			cpd[i] = 0.5f * cpd[i - 1];
		double csf_max = csf(3.248d, 100.0d);

		double F_freq[] = new double[MAX_PYR_LEVELS - 2];
		for (i = 0; i < MAX_PYR_LEVELS - 2; i++)
			F_freq[i] = (csf_max / csf(cpd[i], 100.0f));

		int pixels_failed = 0;
		for (y = 0; y < h; y++) {
			for (x = 0; x < w; x++) {
				int index = x + y * w;
				double contrast[] = new double[MAX_PYR_LEVELS - 2];
				double sum_contrast = 0;
				for (i = 0; i < MAX_PYR_LEVELS - 2; i++) {
					double n1 = Math.abs(la.getValue(x, y, i)
							- la.getValue(x, y, i + 1));
					double n2 = Math.abs(lb.getValue(x, y, i)
							- lb.getValue(x, y, i + 1));
					double numerator = (n1 > n2) ? n1 : n2;
					double d1 = Math.abs(la.getValue(x, y, i + 2));
					double d2 = Math.abs(lb.getValue(x, y, i + 2));
					double denominator = (d1 > d2) ? d1 : d2;
					if (denominator < 1e-5f)
						denominator = 1e-5f;
					contrast[i] = numerator / denominator;
					sum_contrast += contrast[i];
				}
				if (sum_contrast < 1e-5)
					sum_contrast = 1e-5f;
				double F_mask[] = new double[MAX_PYR_LEVELS - 2];
				double adapt = la.getValue(x, y, adaptation_level)
						+ lb.getValue(x, y, adaptation_level);
				adapt *= 0.5f;
				if (adapt < 1e-5)
					adapt = 1e-5f;
				for (i = 0; i < MAX_PYR_LEVELS - 2; i++) {
					F_mask[i] = mask(contrast[i] * csf(cpd[i], adapt));
				}
				double factor = 0;
				for (i = 0; i < MAX_PYR_LEVELS - 2; i++) {
					factor += contrast[i] * F_freq[i] * F_mask[i]
							/ sum_contrast;
				}
				if (factor < 1)
					factor = 1;
				if (factor > 10)
					factor = 10;
				double delta = Math.abs(la.getValue(x, y, 0)
						- lb.getValue(x, y, 0));
				boolean pass = true;
				// pure luminance test
				if (delta > factor * tvi(adapt)) {
					pass = false;
				} else if (!luminanceOnly) {
					// CIE delta E test with modifications
					double color_scale = colorfactor;
					// ramp down the color test in scotopic regions
					if (adapt < 10.0f) {
						// Don't do color test at all.
						color_scale = 0.0f;
					}
					double da = a_labImages[index].getA()
							- b_labImages[index].getA();
					double db = a_labImages[index].getB()
							- b_labImages[index].getB();
					da = da * da;
					db = db * db;
					double delta_e = (da + db) * color_scale;
					if (delta_e > factor) {
						pass = false;
					}
				}
				if (!pass) {
					pixels_failed++;
				}
			}
		}

		System.out.printf("%d pixels are different\n", pixels_failed);

		if (pixels_failed < threshold) {
			errorStr = "Images are perceptually indistinguishable\n";
			return true;
		}

		errorStr = "Images are visibly different\n";

		return false;
	}

}

class XYZImage {
	double x;
	double y;
	double z;

	public XYZImage() {

	}

	public XYZImage(double x, double y, double z) {
		this.x = x;
		this.y = y;
		this.z = z;
	}

	public double getX() {
		return x;
	}

	public void setX(double x) {
		this.x = x;
	}

	public double getY() {
		return y;
	}

	public void setY(double y) {
		this.y = y;
	}

	public double getZ() {
		return z;
	}

	public void setZ(double z) {
		this.z = z;
	}
}

class LABImage {
	double l;
	double a;
	double b;

	public LABImage() {

	}

	public LABImage(double l, double a, double b) {
		this.l = l;
		this.a = a;
		this.b = b;
	}

	public double getL() {
		return l;
	}

	public void setL(double l) {
		this.l = l;
	}

	public double getA() {
		return a;
	}

	public void setA(double a) {
		this.a = a;
	}

	public double getB() {
		return b;
	}

	public void setB(double b) {
		this.b = b;
	}
}

class LPyramid {

	final int MAX_PYR_LEVELS = 8;
	double levels[][];
	int width = 0;
	int height = 0;

	public LPyramid(LABImage[] a_labImages, int w, int h) {
		// Make the Laplacian pyramid by successively
		// copying the earlier levels and blurring them
		width = w;
		height = h;
		levels = new double[MAX_PYR_LEVELS][width * height];
		for (int i = 0; i < MAX_PYR_LEVELS; i++) {
			if (i == 0) {
				levels[i] = copy(getLs(a_labImages));
			} else {
				levels[i] = new double[width * height];
				convolve(levels[i], levels[i - 1]);
			}
		}
	}

	private double[] getLs(LABImage[] a_labImages) {
		double[] ls = new double[a_labImages.length];
		for (int i = 0; i < a_labImages.length - 1; i++) {
			ls[i] = a_labImages[i].getL();
		}
		return ls;
	}

	private void convolve(double a[], double b[]) {
		int y, x, i, j, nx, ny;
		double Kernel[] = { 0.05f, 0.25f, 0.4f, 0.25f, 0.05f };

		for (y = 0; y < height; y++) {
			for (x = 0; x < width; x++) {
				int index = y * width + x;
				a[index] = 0.0f;
				for (i = -2; i <= 2; i++) {
					for (j = -2; j <= 2; j++) {
						nx = x + i;
						ny = y + j;
						if (nx < 0)
							nx = -nx;
						if (ny < 0)
							ny = -ny;
						if (nx >= width)
							nx = 2 * width - nx - 1;
						if (ny >= height)
							ny = 2 * height - ny - 1;
						a[index] += Kernel[i + 2] * Kernel[j + 2]
								* b[ny * width + nx];
					}
				}
			}
		}
	}

	private double[] copy(double[] fs) {
		int max = width * height;
		double[] out = new double[max];
		for (int i = 0; i < max; i++)
			out[i] = fs[i];

		return out;
	}

	public double getValue(int x, int y, int level) {
		int index = x + y * width;
		int l = level;
		if (l > MAX_PYR_LEVELS)
			l = MAX_PYR_LEVELS;
		return levels[level][index];
	}
}
